package digital_cash_system.bank;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.security.AlgorithmParameterGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.spec.DSAParameterSpec;
import java.security.spec.InvalidParameterSpecException;
import java.util.Date;
import java.util.Random;

import digital_cash_system.Coin;
import digital_cash_system.Logger;
import digital_cash_system.PublishedHash;
import digital_cash_system.Utils;
import digital_cash_system.shop.DepositInfo;

public class Bank extends BankBase
{
	private final Random random = new Random();

	// --- cryptographic parms
	// primes
	private BigInteger p;
	private BigInteger q;
	// generators
	private BigInteger g;
	private BigInteger g1;
	private BigInteger g2;
	// public key
	private BigInteger h;
	// private key
	private BigInteger x;

	// ---

	public Bank()
	{
		// initial value for generating g1 and g2
		final int initHGen = 3;

		// DSA parms can be used
		DSAParameterSpec dsaParameterSpec = null;
		try
		{
			dsaParameterSpec = (DSAParameterSpec) AlgorithmParameterGenerator.getInstance("DSA")
					.generateParameters().getParameterSpec(DSAParameterSpec.class);
		} catch (InvalidParameterSpecException | NoSuchAlgorithmException e)
		{
			e.printStackTrace();
		}

		// --- setup parms
		p = dsaParameterSpec.getP();
		q = dsaParameterSpec.getQ();
		g = dsaParameterSpec.getG();
		BigInteger hGen = BigInteger.valueOf(initHGen);
		do
		{
			g1 = hGen.modPow(p.subtract(BigInteger.ONE).divide(q), p);
			hGen = hGen.add(BigInteger.ONE);
		} while (g1.equals(g) || g1.equals(BigInteger.ONE));
		do
		{
			g2 = hGen.modPow(p.subtract(BigInteger.ONE).divide(q), p);
			hGen = hGen.add(BigInteger.ONE);
		} while (g2.equals(BigInteger.ONE));
		x = new BigInteger(q.bitLength(), random);
		h = g.modPow(x, p);
		// ---

	}

	protected void onOpenAccount(Socket clientSocket, ObjectInputStream inputStream)
	{
		try
		{
			ObjectOutputStream outputStream = new ObjectOutputStream(clientSocket.getOutputStream());

			// send public data
			outputStream.writeObject(new Publishedinfo(p, q, g, g1, g2, h));
			outputStream.flush();

			// receive h_u
			BigInteger h_u = (BigInteger) inputStream.readObject();

			// --- Logger output
			Logger.getInstance().write("Receive h_u from Customer or Shop = " + h_u);
			// ---

			// send z
			BigInteger z = h_u.multiply(g2).modPow(x, p);
			outputStream.writeObject(z);

			// --- Logger output
			Logger.getInstance().write("Send to Shop or Customer z = " + z);
			// ---

			// TODO store account info

			// --- Logger output
			Logger.getInstance().write("Account opened");
			// ---

			inputStream.close();
			outputStream.close();
			clientSocket.close();
		} catch (IOException | ClassNotFoundException e)
		{
			// --- Logger output
			Logger.getInstance().write(e.toString());
			// ---
			e.printStackTrace();
		}
	}

	protected void onDeposit(Socket clientSocket, ObjectInputStream inputStream)
	{
		try
		{
			// identify shop
			int shopId = inputStream.readInt();

			// --- Logger output
			Logger.getInstance().write("Requested from " + shopId);
			// ---

			// receive coin, r1, r2, dateTime
			DepositInfo depositInfo = (DepositInfo) inputStream.readObject();
			Coin coin = depositInfo.getCoin();
			BigInteger r1 = depositInfo.getR1();
			BigInteger r2 = depositInfo.getR2();
			Date dateTime = depositInfo.getDateTime();

			// --- Logger output
			Logger.getInstance().write("Receive from Customer r1 = " + depositInfo.getR1());
			// --
			// --- Logger output
			Logger.getInstance().write("Receive from Customer r2 = " + depositInfo.getR2());
			// --
			// --- Logger output
			Logger.getInstance().write("Receive from Customer DateTime = " + depositInfo.getDateTime());
			// --

			BigInteger d = PublishedHash.hashH0(coin.getA(), coin.getB(), shopId, dateTime, q);
			BigInteger c_ = PublishedHash.hashH(coin.getA(), coin.getB(), coin.getZ_(),
					coin.getA_(), coin.getB_(), q);

			if (coin.getA().modPow(d, p).multiply(coin.getB()).mod(p)
					.equals(g1.modPow(r1, p).multiply(g2.modPow(r2, p)).mod(p))
					&& g.modPow(coin.getR_(), p).equals(
							coin.getA_().multiply(h.modPow(c_, p)).mod(p))
					&& coin.getA().modPow(coin.getR_(), p)
							.equals(coin.getB_().multiply(coin.getZ_().modPow(c_, p)).mod(p)))
			{
				// TODO accept coin
				// --- Logger output
				Logger.getInstance().write("Coin accepted");
				Logger.getInstance().write("Deposit completed successfully");
				// ---
			}
		} catch (IOException | ClassNotFoundException e1)
		{
			e1.printStackTrace();
		}
	}

	protected void onWithdraw(Socket clientSocket, ObjectInputStream inputStream)
	{
		try
		{
			ObjectOutputStream outputStream = new ObjectOutputStream(clientSocket.getOutputStream());

			// --- --- verify user
			// --- Logger output
			Logger.getInstance().write("Verify user");
			// ---
			BigInteger h_u = (BigInteger) inputStream.readObject();
			BigInteger y = (BigInteger) inputStream.readObject();
			BigInteger c_r = Utils.generateZ_qMember(q);
			outputStream.writeObject(c_r);
			BigInteger r_r = (BigInteger) inputStream.readObject();
			boolean userAccepted = y.multiply(h_u.modPow(c_r, p)).mod(p).equals(g1.modPow(r_r, p));
			// --- ---
			if (userAccepted)
			{
				// --- --- user accepted
				// --- Logger output
				Logger.getInstance().write("User accepted");
				// ---
				BigInteger w = Utils.generateZ_qMember(q);

				BigInteger a = g.modPow(w, p);
				BigInteger b = h_u.multiply(g2).modPow(w, p);

				// send a, b
				outputStream.writeObject(a);
				outputStream.writeObject(b);

				// --- Logger output
				Logger.getInstance().write("Send to Customer a = " + a);
				// ---

				// --- Logger output
				Logger.getInstance().write("Send to Customer b = " + b);
				// ---

				// receive c
				BigInteger c = (BigInteger) inputStream.readObject();

				// --- Logger output
				Logger.getInstance().write("Receive from Customer c = " + c);
				// ---

				BigInteger r = w.add(c.multiply(x)).mod(q);

				// send r
				outputStream.writeObject(r);

				// --- Logger output
				Logger.getInstance().write("Send to Customer r = " + r);
				// ---

				// --- Logger output
				Logger.getInstance().write("Withdraw completed successfully");
				// ---
				// --- ---
			}

			inputStream.close();
			outputStream.close();
			clientSocket.close();
		} catch (IOException | ClassNotFoundException e)
		{
			e.printStackTrace();
		}
	}
}
